[org 0x1000]  ; 将loader代码放在内存0x1000的位置，是基于实模式的内存布局约束指定
dw 0x55aa  ; 调试魔数，用于判断错误，当boot读入0x1000位置时，得是0x55aa才认为正确，否则认为扇区有错误，防止错误蔓延

; 打印字符串
mov si, loading
call print
;jmp run_loader_err  ; 测试运行加载器错误打印，测试后注释去掉

; ####################检测函数标签##############
detect_memory:
    xor ebx, ebx  ; 使用异或的方式将ebx置为0（也即将要用的寄存器清空），异或是一种比较高效的方式
    ; 然后将ES段寄存器置0，ES:DI 是结构体的缓冲区
    mov ax, 0
    mov es, ax  ; 实际中，无法直接给段寄存器执行mov es,0 , 因为语法上有有歧义，不能确定是将8位还是16位的0赋值给es，而ax是确定的16位寄存器，执行本行给16位的es赋值0
    mov edi, ards_buffer
    mov edx, 0x534d4150  ; 固定签名
    ; 初始化结束后开始检测，定义个一个检测标签next
.next:
    mov eax, 0xe820  ; 指定0x15系统调用子功能号0xe820，为内存检测
    mov ecx, 20  ; ards结构的大小，单位是字节
    int 0x15  ; 调用0x15系统调用
    ; 调用开始后，不断检测返回值，直到检测结束
    jc run_loader_err  ; jc指令检测CF是否置位，如果置位，即CF=1，表示出错，则跳转到错误打印，然后退出
    ; 否则，CF=0，检测正确，则将缓存指针指向下一个结构体
    add di, cx  ; cx是ecx的低16位，前面已初始化为20，所以这里加20
    inc word [ards_count]  ; 将ards_count变量按字加1，[ards_count] 表示操作的是 ards_count 标签所指向的存储单元，间接寻址
    cmp ebx, 0  ; 每次检测后ebx与0比较，如果是0，则表示结束了，如果不是，则还要继续
    jnz .next  ; 上一行如果ebx-0!=0，则零标志位（ZF）=0,jnz就是不为0则跳转到.next标签开始继续执行，如果ebx-0==0，则零标志位（ZF）被置1，表示ebx是0，不跳转
    mov si, detect_mem_succ  ; 上一行没有跳转，则继续执行到这行，表示检测结束，将要打印的内容放入缓冲区，调用print打印
    call print  ; 打印后然后结束
    ; 为了方便观察内存检测到的结果，这里添加一些打印，读取出内存的信息
    ;xchg bx, bx  ; bochs的断点暂停调试
    mov cx, [ards_count]  ; ards_count是一个16位的内存地址，将该地址的16位数据拷贝给CX，也就是上面读到的ards结构体的数量
    mov si, 0  ; 用于结构体指针，SI（Source Index）寄存器是 16 位的通用寄存器。它通常与数据段寄存器（DS）一起使用，用于在内存寻址过程中指示源操作数的偏移地址。在 32 位模式下，它可以扩展为 32 位的 ESI 寄存器，在 64 位模式下则进一步扩展为 64 位的 RSI 寄存器。
.show:
    mov eax, [ards_buffer + si]  ; 相当于对ards_buffer数组取索引，类似于ards_buffer[si]，0到8字节是每个ards的基地址，
    mov ebx, [ards_buffer + si + 8]  ; 类似于ards_buffer[si+8]，根据格式定义，8到16字节是内存的长度
    mov edx, [ards_buffer + si + 16]  ; 这里用edx是因为ecx已经用于存[ards_count]，这里是最后4字节，是内存的类型，其中1表示可用
    ; print ards
    mov dx, 0
    .print_next:
        mov al, [ards_buffer + si]
        mov dh, al
        rol al, 4
        call covert_print_char
        mov al, dh
        call covert_print_char
        add si, 1
        add dx, 1
        cmp dx, 20
        jnz .print_next
    mov al, ','
    mov ah, 0x0e
    int 0x10
    ; add si, 20  ; 每次循环将si加一个ards结构体的长度20字节
    ;xchg bx, bx  ; bochs的断点暂停调试，每次循环后调试看下
    loop .show  ; loop指令，会自动根据cx/ecx的值判断循环次数，当cx值为0时结束循环，否则会对cx减1然后继续跳转到循环入口，直到cx减到0时结束循环
    mov si, print_mem_succ
    call print

jmp $  ; 阻塞

loading:
    db "loader: loading on...", 10, 13, 0 ; 分别是 \n(换行) \r（将光标移动到气势位置） 0（字符串结束）
detect_mem_succ:  ; 检测内存成功的打印内容
    db "loader: detect memory success", 10, 13, 0 ; 分别是 \n(换行) \r（将光标移动到气势位置） 0（字符串结束）
print_mem_succ:  ; 检测内存成功的打印内容
    db "", 10, 13, 0 ; 分别是 \n(换行) \r（将光标移动到气势位置） 0（字符串结束）

print:
    mov ah, 0x0e ; 设置的前置条件，表示要调用int 0x10 的打印一个字符的功能
    ; 创建一个循环打印
    .next:
        mov al, [si] ; 间接寻址，将si指向的字符放入al
        cmp al, 0 ; 和0比较，如果是0则表示字符串最后一个字符，循环结束
        jz .return ; jump if zero，当零标志位ZF为1时，执行跳转操作， cmp的两个操作数的值相等（相减为0），ZF标志位会被设置为1，则跳转到.return。
        ;jz指令的另一种写法是je指令，即jump if equal，它的作用与jz指令完全相同
        int 0x10  ; 如果不是最后一个0则要调用0x10中断bios的打印功能继续打印
        inc si ; 将si+1
        jmp .next
    .return:
    ret

covert_print_char:
    and al, 0x0f
    add al, '0'
    cmp al, '9'
    jle .print_char
    add al, 7
    .print_char:
        mov ah, 0x0e
        int 0x10
    ret

run_loader_err:  ; 运行加载器错误
    mov si, .err_msg
    call print
    hlt  ; cpu停止指令
    jmp $
    .err_msg:
        db "run loader program error!", 10, 13, 0
    ret

; 在最后增加两个变量数据
ards_count:  ; 用于存储内存的数量，一个字的size
    dw 0
ards_buffer:  ; 用于存储内存信息的buffer，这个一定要放在最后，因为运行前不清楚具体有多少个内存信息，
              ; 比如bochs有6个、vmware有11个，具体的实体机/虚拟机描述符的数量不定，为了防止覆盖buffer之后的内容，
              ; 放在最后才合理。若buffer溢出导致内容覆盖出错，容易导致不可预期的错误，很难调试。
